package edu.mtu.citizenscience.cs.utils.gps;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;

import edu.mtu.citizenscience.cs.utils.Services;
import edu.mtu.citizenscience.cs.utils.exceptions.GPSNotAvailable;
/**
 * @author Corbin Uselton <corbinu@cwuselto@mtu.edu>
 * @copyright (c) 2012 MichiganTech <http://www.mtu.edu>
 * @since 1.0
 * 
 * Interacts the GPS service making it easy to get the devices location
 */
public class CitsciGPS {
	// messenger to the GPS service
	private Messenger messenger;
	// the most recent request for a location
	private CitsciGPSRequest request;
	
	/**
	 * @since 1.0
	 * 
	 * The rate at which the GPS should be getting updates
	 */
	public enum UpdateRate {
		TIGHT, LOOSE;
	}
	
	/**
	 * @since 1.0
	 * 
	 * Handles messages from the GPS service
	 */
	@SuppressLint("HandlerLeak")
	private Handler handler = new Handler() 
	{
		
		/**
		 * Called when a message from the GPS service is received
		 * 
		 * @param   message - Message - The message from the service
		 * @return	void
		 */
		public void handleMessage(Message message) 
		{
			// get the data bundle from the message
			Bundle data = message.getData();
			// if the service is working properly and sent a data bundle
			if (message.arg1 == Activity.RESULT_OK && data != null) 
			{
				// get all fields of sent location
				String provider = data.getString(CitsciGPSService.PROVIDER);
				Double latitude = data.getDouble(CitsciGPSService.LATITUDE);
				Double longitude = data.getDouble(CitsciGPSService.LONGITUDE);
				Double altitude = data.getDouble(CitsciGPSService.ALTITUDE);
				Float bearing = data.getFloat(CitsciGPSService.BEARING);
				Float speed = data.getFloat(CitsciGPSService.SPEED);
				Long time = data.getLong(CitsciGPSService.TIME);
				Float accuracy = data.getFloat(CitsciGPSService.ACCURACY);
				
				// check if provider was passed
				if (provider == null)
				{
					// default the provider to GPS
					provider = "GPS";
				}
				// create a new location with the provider
				Location location = new Location(provider);
				
				// check if latitude was passed
				if (latitude != null)
				{
					// add the latitude to the new location
					location.setLatitude(latitude);
				}
				// check if longitude was passed
				if (longitude != null)
				{
					// add the longitude to the new location
					location.setLongitude(longitude);
				}
				// check if altitude was passed
				if (altitude != null)
				{
					// add the altitude to the new location
					location.setAltitude(altitude);
				}
				// check if bearing was passed
				if (bearing != null)
				{
					// add the bearing to the new location
					location.setBearing(bearing);
				}
				// check if speed was passed
				if (speed != null)
				{
					// add the speed to the new location
					location.setSpeed(speed);
				}
				// check if time was passed
				if (time != null)
				{
					Log.d("GPS", "Received location from time: " + time);	
					// add the time to the new location
					location.setTime(time);
				}
				// check if accuracy was passed
				if (accuracy != null)
				{
					// add the accuracy to the new location
					location.setAccuracy(accuracy);
				}
				// pass the new location update
				locationUpdate(location);
			}
		}
	};
	
	/**
	 * @since 1.0
	 * 
	 * Handles connecting and disconnecting from the GPS service
	 */
	private ServiceConnection conn = new ServiceConnection() 
	{
		/**
		 * Called when the GPS service is connected
		 * 
		 * @param   className - ComponentName - The concrete component name of the service that has been connected.
		 * @param   binder - IBinder - The IBinder of the Service's communication channel, which you can now make calls on.
		 * @return	void
		 */
	    public void onServiceConnected(ComponentName className, IBinder binder) 
	    {
	    	// create a new message from the service binder to send messages over
	    	messenger = new Messenger(binder);
	    }
	
	    /**
		 * Called when the GPS service is disconnected
		 * 
		 * @param   className - ComponentName - The concrete component name of the service whose connection has been lost.
		 * @return	void
		 */
		public void onServiceDisconnected(ComponentName className) 
		{
			// service disconnected so clear messenger
			messenger = null;
	    }
	};

	/**
	 * Start the GPS service called from App Create method
	 * 
	 * @param   context - Context - the app to create the service for
	 * @return	void
	 */
	public static void create(Context context)
	{
		// start the GPS service
        context.startService(new Intent(context, CitsciGPSService.class));
	}
	
	/**
	 * Binds the app to the GPS service called in the activities resume method
	 * 
	 * @param   context - Context - the activity to bind the GPS Service to
	 * @return	void
	 * @throws	GPSNotAvailable
	 */
	public void resume(Context context) throws GPSNotAvailable
	{
		// new GPS service intent
		Intent intent = new Intent(context, CitsciGPSService.class);
		// create new messenger for this GPS Service handler
		Messenger messenger = new Messenger(handler);
		// put the messenger in the intent
		intent.putExtra(Services.MESSENGER, messenger);

		// bind the service to this object using the ServiceConnection using the intent
		context.bindService(intent, conn, Context.BIND_AUTO_CREATE);
	}
	
	/**
	 * Unbinds the app from the GPS service called in the activities pause method
	 * 
	 * @param   context - Context - the activity to bind the GPS Service to
	 * @return	void
	 */
	public void pause(Context context)
	{
		// unbind the service from this object using the ServiceConnection
		context.unbindService(conn);
	}
	
	/**
	 * Checks if the GPS is enabled
	 * 
	 * @param   context - Context - the activity to check with
	 * @return	boolean
	 */
	public static boolean isEnabled(Context context)
	{
		// return whether the GPS Provider is enabled or not
		return ((LocationManager) context.getSystemService(Context.LOCATION_SERVICE)).isProviderEnabled(LocationManager.GPS_PROVIDER);
	}
	
	/**
	 * Sets the GPS update rate
	 * 
	 * @param   rate - UpdateRate - enum to set the GPS update rate
	 * @return	void
	 */
	public void setUpdateRate(UpdateRate rate)
	{
		// check if the service messenger is attached or not
		if (this.messenger != null)
		{	
			// get a new message
			Message msg = Message.obtain();
			
			// create a new message data bundle
			Bundle bundle = new Bundle();
			
			// switch over the possible update rates
			switch (rate) 
			{
				case LOOSE:
					// attach the LOOSE update rate to the message bundle
					bundle.putString(CitsciGPSService.SET_UPDATE_RATE, CitsciGPSService.LOOSE);
				break;
				case TIGHT:
					// attach the TIGHT update rate to the message bundle
					bundle.putString(CitsciGPSService.SET_UPDATE_RATE, CitsciGPSService.TIGHT);
				break;
			}

			try 
			{
				// add the bundle data to the message
				msg.setData(bundle);
				// send the message to the service
				messenger.send(msg);
			} 
			catch (RemoteException e) 
			{
				e.printStackTrace();
			}
		}
		else
		{
			Log.e("GPS", "Attempting to send message to GPSService but its messager is not attached");
		}
	}
	
	/**
	 * Request a location from the GPS Service
	 * 
	 * @param   request - CitsciGPSRequest - the callback for the GPS Request
	 * @return	void
	 */
	public void requestLocation(CitsciGPSRequest request)
	{
		this.request = request;
		// check if the service messenger is attached or not
		if (this.messenger != null)
		{
			// get a new message
			Message msg = Message.obtain();
			
			try 
			{
				// send the message to the service
				messenger.send(msg);
			} 
			catch (RemoteException e) 
			{
				e.printStackTrace();
			}
		}
		else
		{
			Log.e("GPS", "Attempting to send message to GPSService but its messager is not attached");
		}
	}
	
	/**
	 * Called when a location is recived from the GPS service
	 * 
	 * @param   location - Location - the location from the GPS Service
	 * @return	void
	 */
	public void locationUpdate(Location location)
	{
		// check if a location request is pending
		if (this.request != null)
		{
			Log.d("GPS", "Location update from time: " + location.getTime());	
			// return the current location
			this.request.locationResult(location);
		}
	}
	
	/**
	 * Cancels the last location request
	 * 
	 * @param   void
	 * @return	void
	 */
	public void cancelRequst()
	{
		// remove the location request
		this.request = null;
	}
	
}
